'use strict';
const Service = require('egg').Service;
const assert = require('assert');


class oidcService extends Service {
  get _userModel() {
    return this.app.model.User;
  }
  get _accessToeknModel() {
    return this.app.model.AccessToken;
  }
  get _refreshToeknModel() {
    return this.app.model.RefreshToken;
  }
  get _clientModel() {
    return this.app.model.Client;
  }
  get _codeModel() {
    return this.app.model.AuthorizationCode;
  }
  get gt() {
    return this.app.Sequelize.Op.gt;
  }
  /**
   * 通过code获取accessToken
   * @param {String} code code
   * @param {String} redirect_uri 回调地址
   * @param {Stromg} clientId 应用Id
   * @param {String} clientSecret 应用key
   */
  async token(code, redirect_uri, clientId, clientSecret) {
    const { ctx, service } = this;
    const codeMsg = await this._codeModel.queryAuthorizationCodeAndRemove({
      code,
      // redirectUri: redirect_uri, // 好像没必要那么严格
    });
    assert(codeMsg && codeMsg.clientId === clientId, 'code无效');
    const client = await this._clientModel.findOne({
      where: {
        clientId: codeMsg.clientId,
      },
    });
    assert(!client || client.clientSecret === clientSecret, 'code无效');
    const user = await this._userModel.findOne({
      where: {
        lid: codeMsg.userId,
      },
    });
    assert(user, 'code无效');
    const now = ctx.helper.toTimeStamp();
    const expires = now + 2 * 60 * 60;

    const payload = {
      iss: 'https://zeeephr.cn',
      sub: user.lid,
      aud: client.clientId,
      exp: expires, // 两小时
      iat: now,
      ava: user.avatar,
      nam: user.name,
    };
    const idToken = service.crypto.jwt(decodeURIComponent(client.privateKey), payload);
    const accessTokenStr = ctx.helper.randomString(12);
    const refreshTokenStr = ctx.helper.randomString(12);
    await ctx.model.AccessToken.saveAccessToken({
      accessToken: accessTokenStr,
      expiresAt  : expires,
      scope      : codeMsg.scope,
    }, client, user);
    await ctx.model.RefreshToken.saveRefreshToken({
      refreshToken         : refreshTokenStr,
      refreshTokenExpiresAt: expires,
      scope                : codeMsg.scope,
    }, client, user);
    return {
      idToken,
      accessToken : accessTokenStr,
      refreshToken: refreshTokenStr,
      expires,
    };
  }
  /**
   * 判断是要跳转到登录授权页面还是跳转成功界面
   * @param {String} clientId 应用Id
   * @param {String} redirectUri 回调地址
   * @param {String} scope openid
   */
  async authorize(clientId, redirectUri, scope) {
    const { ctx } = this;
    try {
      if (!ctx.session.user) {
        return false;
      }
      const user = await this._userModel.findByLid(this.ctx.session.user.lid);
      if (!user) {
        delete ctx.session.user;
        throw new assert.AssertionError({ message: '用户已注销' });
      }
      const code = await this._codeModel.queryAuthorizationCode({ // 密码填写正确之后会存入code
        clientId,
        userId: user.lid,
        redirectUri,
      });
      ctx.logger.debug(`authorize-------------queryAuthorizationCode----------------${JSON.stringify(code)}`);
      if (code) {
        return code.code;
      }
      const client = await this._clientModel.findOne({
        where: { clientId, redirectUri },
      });
      assert(client, 'redirectUri错误');
      const accessToken = await this._accessToeknModel.getAccessTokenByUser(user.lid); // 关联登录
      if (accessToken) {
        const codeStr = ctx.helper.randomString(12);
        await this._clientModel.saveAuthorizationCode({
          code       : codeStr,
          expiresAt  : new Date(new Date() + 2 * 60 * 1000), // 两个小时之后过期
          redirectUri: code.redirectUri,
          scope,
        }, client, user);
        return codeStr;
      }
      return false;
    } catch (err) {
      delete ctx.session.user;
      ctx.logger.warn(`service authorize方法异常 err: ${err}`);
      return false;
    }
  }
  /**
   * 注册
   * @param {String} username 用户名
   * @param {String} password 密码
   * @param {String} lid 用户长id
   * @param {String} appId 应用id
   * @param {String} redirectUri 回调地址
   */
  async register(username, password, lid = '', appId, redirectUri) {
    this.ctx.logger.debug(username, password);
    let user;
    if (lid) {
      user = await this._userModel.findByLid(lid);
    } else {
      lid = this.ctx.helper.randomString(9, true);
      while (lid[0] === '0' || await this._userModel.findOne({ where: { lid } })) {
        lid = this.ctx.helper.randomString(9, true);
      }
    }
    assert(!user, '账号已存在');
    const hashedPassword = await this.ctx.helper.genHash(password);
    await this._userModel.register({
      lid,
      name: username,
      hashedPassword,
    });
    await this._codeModel.saveAuthorizationCode({
      authorizationCode: this.ctx.helper.randomString(12),
      expiresAt        : new Date(new Date() + 2 * 60 * 1000), // 两个小时之后过期
      scope            : 'openid',
      redirectUri,
    }, { clientId: appId }, { lid });
    this.ctx.session.user = {
      lid,
    };
  }
  /**
   * 检查密码
   * @param {String} lid 用户长id
   * @param {String} password 密码
   * @param {String} appId 应用id
   * @param {String} redirectUri 回调地址
   */
  async checkPsw(lid, password,  appId, redirectUri) {
    const user = await this._userModel.findByLid(lid);
    assert(user, '账号或密码错误');
    const same = await this.ctx.helper.compare(password, user.hashedPassword);
    assert(same, '用户名或密码错误');
    const client = await this._clientModel.queryClient({ clientId: appId });
    await this._codeModel.saveAuthorizationCode({
      authorizationCode: this.ctx.helper.randomString(12),
      expiresAt        : new Date(new Date() + 2 * 60 * 1000), // 两个小时之后过期
      scope            : 'openid',
      redirectUri,
    }, client, { lid: user.lid });
    this.ctx.session.user = {
      lid,
    };
  }

  /**
   * 登出全部应用
   * @param {String} lid 用户长id
   * @param {String} accessToken token
   * @param {String|undefined} clientId 应用id
   */
  async logout(lid, accessToken, clientId) {
    const exit = await this._accessToeknModel.findOne({
      where: {
        userId              : lid,
        accessToken,
        accessTokenExpiresAt: {
          [this.gt]: this.ctx.helper.toTimeStamp(),
        },
      },
    });
    if (!exit) {
      this.ctx.throw(403, 'access_token 无效');
    }
    const where = {
      userId: lid,
    };
    if (clientId) {
      Object.assign(where, { clientId });
    }
    await this._accessToeknModel.destroy({ where });
    await this._refreshToeknModel.destroy({ where });
  }
}

module.exports = oidcService;

